
在上篇文章中,我已稍微提及了本次專案所使用的驗證工具——Zod。Zod 是專為 TypeScript 設計的資料驗證工具,就像人體的免疫系統,確保資料結構在 runtime 時與 TypeScript 的靜態型別一致,彌補了 TypeScript 僅在編譯時檢查靜態型別的不足。Zod 主打的特色是可以藉由設置驗證的 Schema ,並通過 TypeScript 的型別推斷功能,自動產生相對應的型別,這避免了開發者重複手動定義型別的需要。除此之外,Zod 還具備一套完整的錯誤訊息系統,大大減少了開發者需要自行定義驗證邏輯和錯誤處理的複雜性。
parse 或 safeParse 驗證和確保外來資料的正確型別。如果熟悉 TypeScript 的使用邏輯,其實在使用 Zod 時會覺得格外親切,例如我想要定義標單中的驗證及型別
先定義 scheme 再轉換為 type。
特別注意因為 TypeScript 的型別規則,所以 Zod 預設就是 required 屬性
const formValuesSchema = z.object({
  customImage: z.string(),
  title: z.string(),
  description: z.string(),
  themeColor: z.enum(['basic', 'blue-rose', 'lime'])
})
type FormValues = z.infer<typeof formValuesSchema>
上方範例中我的 FormValues 就會是:
type FormValues = {
    title: string;
    customImage: string;
    description: string;
    themeColor: "basic" | "blue-rose" | "lime";
}
設定好了規則,我們還需要設定錯誤訊息,
required_error
invalid_type_error
const loginSchema = z.object({
  email: z.string({
    required_error: 'Email 為必填欄位',
    invalid_type_error: '必須為字串'
  }),
  password: z.string({ required_error: 'Password 為必填欄位' })
})
想要依據不同狀態設置不同訊息,Zod 也提供 errorMap 屬性可以使用
issue code 可以參考:https://github.com/colinhacks/zod/blob/master/ERROR_HANDLING.md#error-handling-in-zod
const loginSchema = z.object({
  email: z.string({
    required_error: 'Email 為必填欄位',
    invalid_type_error: '必須為字串',
    description: '請輸入正確的 Email',
    errorMap: (issue, ctx) => {
      if (issue.code === z.ZodIssueCode.invalid_type) {
        if (issue.received === 'undefined') { 
          return { message: 'Email 為必填欄位' }
        }
        if (issue.received === 'number') {
          return { message: '不可為數字' }
        }
      }
      return { message: ctx.defaultError }
    }
  }),
  password: z.string({ required_error: 'Password 為必填欄位' })
})
假如我希望 string、number 等型別可以有像是長度或是特殊規格,Zod 也有提供許多常用方法:
const signUpSchema = z.object({
  username: z
    .string({ required_error: 'Username 為必填欄位' })
    .regex(/^[a-zA-Z0-9_]*$/),
  email: z
    .string({ required_error: 'Email 為必填欄位' }).email(),
  password: z
    .string({ required_error: 'Password 為必填欄位' }).min(8)
})
另外看到很特別的有 url()、emoji()、ip()、includes(string)
number 類型則有 gt(number)、gte(number)、lt(number)、gt(number)、lte(number)、positive()、negative() 等方法
一樣將 message 寫在參數中即可
const signUpSchema = z.object({
  username: z
    .string({ required_error: 'Username 為必填欄位' })
    .regex(
      /^[a-zA-Z0-9_]*$/,
      '只能包含英文、數字及底線,不可包含空白及特殊符號'
    ),
  email: z
    .string({ required_error: 'Email 為必填欄位' })
    .email('請輸入正確的 Email'),
  password: z
    .string({ required_error: 'Password 為必填欄位' })
    .min(8, '密碼長度不可小於 8 個字元')
})

Zod 提供了兩種 null 的方法
nullable() :數值可以為 nullnullish() :數值可以為 null 或 undefinedconst schema = z.object({
  customImage: z.string().url().nullable(),
  title: z.string().nullable(),
  description: z.string().max(300, '字數不可超過 300 字').nullable(),
  themeColor: z.enum(['basic', 'blue-rose', 'lime'])
})
我的範例中有使用到 enum 的型別,Zod 除了提供自己的方法也可以使用原生的 enum 設定
使用 nativeEnum 定義 schema
enum ThemeColor {
	BASIC = 'basic',
	BLUE_ROSE = 'blue-rose',
	LIME = 'lime'
}
const themeColor = z.nativeEnum(ThemeColor)
const themeColor = z.enum(['basic', 'blue-rose', 'lime'])
// 或是
const COLORS = ['basic', 'blue-rose', 'lime'] as const
const themeColor = z.enum(COLORS)
Zod 提供一個方法 - catch() 可以將錯誤的值替換為希望的值
範例說明:如果
themeColor不為'basic', 'blue-rose', 'lime',設為‘basic’
const schema = z.object({
  customImage: z.string().url().nullable(),
  title: z.string().nullable(),
  description: z.string().max(300, '字數不可超過 300 字').nullable(),
  themeColor: z.enum(['basic', 'blue-rose', 'lime']).catch('basic')
})
如同 typeScript 的 Record 泛型,Zod 也可以使用這個方法。第一個參數為 key 的型別,第二參數為 value 型別
const genericFieldsSchema = z.record(z.string(), z.string().nullable())
可以使用 union()、merge() 或 extends()
union()建立一個聯合型別(union type)
const signUpSchema = z.object({
  username: z
    .string({ required_error: 'Username 為必填欄位' })
    .regex(/^[a-zA-Z0-9_]*$/),
  email: z
    .string({ required_error: 'Email 為必填欄位' }).email(),
  password: z
    .string({ required_error: 'Password 為必填欄位' }).min(8)
})
const genericFieldsSchema = z.record(z.string(), z.string().nullable())
const unionSchema = z.union([signUpSchema, genericFieldsSchema])
extend()可以從一個現有的 schema 繼承,並添加或覆蓋 field。
const baseSchema = z.object({
  username: z
    .string({ required_error: 'Username 為必填欄位' })
    .regex(/^[a-zA-Z0-9_]*$/),
	password: z
	    .string({ required_error: 'Password 為必填欄位' }).min(8)
});
const extendedSchema = baseSchema.extend({
	 email: z
	    .string({ required_error: 'Email 為必填欄位' }).email(),
});
merge()合併兩個 schema,但不支持 field 的覆蓋
const baseSchema = z.object({
  username: z
    .string({ required_error: 'Username 為必填欄位' })
    .regex(/^[a-zA-Z0-9_]*$/),
	password: z
	    .string({ required_error: 'Password 為必填欄位' }).min(8)
});
const emailSchema = z.object({
	 email: z.string({ required_error: 'Email 為必填欄位' }).email(),
});
const mergedSchema = baseSchema.merge(emailSchema);
Zod 當然也提供了除了正則以外的自定義規則方法 - refine() 或 superRefine()
refine:回傳一個布林值來表示驗證是否成功,第一個參數為自定義驗證的 callback,第二個參數則是錯誤訊息。superRefine:callback 提供兩個參數:
⚠️ 特別注意如果回傳錯誤請使用
ctx.addIssue()先判斷錯誤類型再輸入錯誤訊息進行錯誤處理,而不是直接回傳布林值!
const schema = z
  .object({
    id: z.string().nullable(),
    title: z.string().nullable(),
    url: z.string().url().nullable(),
    type: z
      .record(z.string(), z.string())
      .catch({ id: 'default', label: '請選擇' }),
    order: z.number().int().nullable()
  })
  .superRefine((val, ctx) => {
    if (val.type.id === 'website') {
      if (!val.title)
        return ctx.addIssue({ // 使用 ctx.addIssue 做錯誤處理
          code: z.ZodIssueCode.custom, // 使用自定義的錯誤類型
          message: '請輸入連結名稱' // 錯誤訊息
        })
    }
  })
通常從 API 回傳的資料型別會是 unknown ,所以我們可以藉由 Zod 提供的 parse 和 safeParse 進行檢驗
ZodError。這表示需要在使用 parse 時使用 try/catch 來處理錯誤。success 布林值,表示驗證是否成功。
success 為 true,回傳的物件也包含一個 data 屬性,該屬性包含驗證過的資料。success 為 false,回傳的物件也包含一個 error 屬性,是一個 ZodError 實例,顯示驗證失敗的原因。// currentUser 是由後端回傳的資料
const themeColorSchema = z.enum(['basic', 'blue-rose', 'lime'])
const currentUserSchema = z.object({
  username: z.string(),
  customImage: z.string().nullable(),
  image: z.string().nullable(),
  title: z.string().nullable(),
  description: z.string().nullable(),
  themeColor: themeColorSchema
})
type CurrentUser = z.infer<typeof currentUserSchema>
const result = currentUserSchema.safeParse(user).success // true or false
const isValidCurrentUser = (user: unknown): user is CurrentUser => {
  return currentUserSchema.safeParse(user).success
}
if (!isValidCurrentUser(currentUser)) {
    return <div>Something went wrong</div>
  }
起初想要使用是因為可以進行表單驗證,但越深入學習越發現他的功能讓開發省了很多程序,也慢慢了解這個工具越來越火紅的道理,這篇文章羅列了一些在專案中使用到的功能,也是在這次開發中新學習到的工具,還有很多其他的功能沒敘述到,也可以在日後慢慢研究。最後想要分享一個關於結合 react-form-hook 的型別問題:
❓在自定義 Input 元件中,我將 react-form-hook 的 register 作為參數傳進來,型別定義為 register: UseFormRegister<FieldValues> ,UseFormRegister 和 FieldValues 皆由 react-form-hook 提供,其中 FieldValues 的型別為
interface FieldValues {
  [x: string]: any
}
以下是 useForm 的 value 定義的 schema
const schema = z.object({
  customImage: z.string().url().nullable(),
  title: z.string().nullable(),
  description: z.string().max(300, '字數不可超過 300 字').nullable(),
  themeColor: z.enum(['basic', 'blue-rose', 'lime']).catch('basic')
})
type FormValues = z.infer<typeof schema>
這時候問題就出現了:type FieldValues = z.infer<typeof schema> 與 Input 元件中的 FieldValues 型別不符合

所以解決的方法是將 FormValues 也加上 FieldValues 的型別:
定義為 genericFieldsSchema
const schema = z.object({
  customImage: z.string().url().nullable(),
  title: z.string().nullable(),
  description: z.string().max(300, '字數不可超過 300 字').nullable(),
  themeColor: z.enum(['basic', 'blue-rose', 'lime']).catch('basic')
})
const genericFieldsSchema = z.record(z.string(), z.string().nullable())
const unionSchema = z.union([schema, genericFieldsSchema])
type FormValues = z.infer<typeof unionSchema>
這是目前想到的解決方法,分享給如果有遇到相同情況的讀者們,如果有更好的方法也歡迎提供!!那麼今日份的進度就到驗證結束了~
https://ithelp.ithome.com.tw/articles/10305282
